/*
 * Copyright 2011 J. Auer, J. Andert, F.-J. Jendrusch, U. Celebi
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package de.fuberlin.replication.communication.bluetooth.android;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.UUID;
import java.util.concurrent.Semaphore;

import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothServerSocket;
import android.bluetooth.BluetoothSocket;
import android.util.Log;
import de.fuberlin.replication.communication.Communication;
import de.fuberlin.replication.communication.CommunicationAlreadyEstablishedException;
import de.fuberlin.replication.communication.CommunicationNotYetEstablishedException;
import de.fuberlin.replication.communication.bluetooth.BluetoothNotEnabledException;
import de.fuberlin.replication.communication.bluetooth.BluetoothNotSupportedException;

/**
 * Android-specific Bluetooth communication implementation.
 */
public class BluetoothCommunication implements Communication {
	private static final String TAG = "BluetoothCommunication";

	private static final String NAME = "replication";

	private enum State {
		IDLE, ESTABLISHED
	}

	private State state = State.IDLE;

	private int waiter = 0;
	private Semaphore wait = new Semaphore(0, true);

	/** Service record UUID to lookup RFCOMM channel. */
	UUID uuid = UUID.fromString("f5e30f10-5002-11e0-b8af-0800200c9a66");

	/** Bluetooth adapter (default). */
	BluetoothAdapter adapter;

	private ConnectThread connector;
	private AcceptThread acceptor;

	private Connection connection;

	/**
	 * Constructor. Checks Bluetooth support and adapter status.
	 * 
	 * @throws BluetoothNotSupportedException if Bluetooth is not supported by the device.
	 * @throws BluetoothNotEnabledException if Bluetooth is not enabled on the device.
	 */
	public BluetoothCommunication() {
		adapter = BluetoothAdapter.getDefaultAdapter();

		if (adapter == null)
			throw new BluetoothNotSupportedException();

		if (!adapter.isEnabled())
			throw new BluetoothNotEnabledException();
	}

	private synchronized State getState() {
		return state;
	}

	private synchronized void setState(State state) {
		this.state = state;

		wait.release(waiter);
		waiter = 0;
	}

	/**
	 * Checks if the communication is established.
	 * 
	 * @return true if the communication is established, false otherwise.
	 */
	public synchronized boolean isEstablished() {
		return getState() == State.ESTABLISHED;
	}

	/**
	 * Causes the current thread to wait until the communication is established, or
	 * {@link BluetoothCommunication#close()} is called.
	 * 
	 * @throws CommunicationNotYetEstablishedException if {@link BluetoothCommunication#close()} was called.
	 */
	public void waitEstablished() {
		if (isEstablished())
			return;

		synchronized (this) {
			waiter++;
		}

		wait.acquireUninterruptibly();

		if (!isEstablished())
			throw new CommunicationNotYetEstablishedException();
	}

	/**
	 * Connects asynchronously to the Bluetooth device with the given address.
	 * 
	 * @param address the Bluetooth device address.
	 * @throws IOException on error, for example Bluetooth not available, or insufficient permissions.
	 * @throws CommunicationAlreadyEstablishedException if the communication is already established.
	 * @throws CommunicationNotYetEstablishedException if an synchronous connect fails.
	 * @see BluetoothCommunication#connect(String, boolean)
	 */
	public synchronized void connect(String address) throws IOException {
		connect(address, true);
	}

	/**
	 * Connects to the Bluetooth device with the given address.
	 * 
	 * @param address the Bluetooth device address.
	 * @param async the asynchronous connect flag.
	 * @throws IOException on error, for example Bluetooth not available, or insufficient permissions.
	 * @throws CommunicationAlreadyEstablishedException if the communication is already established.
	 * @throws CommunicationNotYetEstablishedException if an synchronous connect fails.
	 * @see BluetoothCommunication#connect(BluetoothDevice, boolean)
	 */
	public synchronized void connect(String address, boolean async) throws IOException {
		connect(adapter.getRemoteDevice(address), async);
	}

	/**
	 * Connects asynchronously to the given Bluetooth device.
	 * 
	 * @param device the Bluetooth device.
	 * @throws IOException on error, for example Bluetooth not available, or insufficient permissions.
	 * @throws CommunicationAlreadyEstablishedException if the communication is already established.
	 * @throws CommunicationNotYetEstablishedException if an synchronous connect fails.
	 * @see BluetoothCommunication#connect(BluetoothDevice, boolean)
	 */
	public synchronized void connect(BluetoothDevice device) throws IOException {
		connect(device, true);
	}

	/**
	 * Connects to the Bluetooth device.
	 * 
	 * @param device the Bluetooth device.
	 * @param async the asynchronous connect flag.
	 * @throws IOException on error, for example Bluetooth not available, or insufficient permissions.
	 * @throws CommunicationAlreadyEstablishedException if the communication is already established.
	 * @throws CommunicationNotYetEstablishedException if an synchronous connect fails.
	 * @see BluetoothDevice#createRfcommSocketToServiceRecord(UUID)
	 */
	public synchronized void connect(BluetoothDevice device, boolean async) throws IOException {
		if (isEstablished())
			throw new CommunicationAlreadyEstablishedException();

		if (connector != null) {
			connector.close();
			connector = null;
		}

		connector = new ConnectThread(device);

		if (async)
			connector.start();
		else {
			connector.run();

			if (!isEstablished())
				throw new CommunicationNotYetEstablishedException();
		}
	}

	/**
	 * Starts listening asynchronously.
	 * 
	 * @throws IOException on error, for example Bluetooth not available, or insufficient permissions, or channel in
	 *             use.
	 * @throws CommunicationAlreadyEstablishedException if the communication is already established.
	 * @see BluetoothAdapter#listenUsingRfcommWithServiceRecord(String, UUID)
	 */
	public synchronized void listen() throws IOException {
		if (isEstablished())
			throw new CommunicationAlreadyEstablishedException();

		if (acceptor == null) {
			acceptor = new AcceptThread();
			acceptor.start();
		}
	}

	/**
	 * On connect callback.
	 * 
	 * @param thread the successful thread (connector or acceptor).
	 * @param socket the socket.
	 */
	synchronized void onConnect(Thread thread, BluetoothSocket socket) {
		if (isEstablished()) {
			Log.e(TAG, "Unnecessary socket");

			try {
				socket.close();
			} catch (IOException e) {
				Log.e(TAG, "Unnecessary socket closing failed", e);
			}

			throw new CommunicationAlreadyEstablishedException();
		}

		try {
			connection = new Connection(socket);
		} catch (IOException e) {
			Log.e(TAG, "Corrupted socket", e);

			try {
				socket.close();
			} catch (IOException e1) {
				Log.e(TAG, "Corrupted socket closing failed", e1);
			}

			throw new CommunicationNotYetEstablishedException();
		}

		if (thread == connector) {
			if (acceptor != null)
				acceptor.close();
		} else if (thread == acceptor)
			if (connector != null)
				connector.close();

		connector = null;
		acceptor = null;

		setState(State.ESTABLISHED);
	}

	@Override
	public synchronized Object read() throws IOException, ClassNotFoundException {
		if (!isEstablished())
			throw new CommunicationNotYetEstablishedException();

		return connection.read();
	}

	@Override
	public synchronized void write(Object o) throws IOException {
		if (!isEstablished())
			throw new CommunicationNotYetEstablishedException();

		connection.write(o);
	}

	/**
	 * Closes any open connection attempt, listener and established connection.
	 */
	public synchronized void close() {
		if (connector != null) {
			connector.close();
			connector = null;
		}

		if (acceptor != null) {
			acceptor.close();
			acceptor = null;
		}

		if (connection != null) {
			connection.close();
			connection = null;
		}

		setState(State.IDLE);
	}

	private class ConnectThread extends Thread {
		private BluetoothSocket client;

		public ConnectThread(BluetoothDevice device) throws IOException {
			client = device.createRfcommSocketToServiceRecord(uuid);
		}

		@Override
		public void run() {
			if (adapter.isDiscovering())
				adapter.cancelDiscovery();

			try {
				client.connect();

				onConnect(this, client);
			} catch (CommunicationAlreadyEstablishedException e) {
				client = null;
			} catch (CommunicationNotYetEstablishedException e) {
				client = null;
			} catch (IOException e) {
				Log.e(TAG, "Connect failed", e);

				close();

				/**
				 * Automatically start listening on connect failure is unnecessary. <code>
				try {
					listen();
				} catch (IOException e1) {
					Log.e(TAG, "Listen failed", e1);
				}
				 * </code>
				 */
			}
		}

		public synchronized void close() {
			if (client != null)
				try {
					client.close();
				} catch (IOException e) {
					Log.e(TAG, "Connector socket closing failed", e);
				} finally {
					client = null;
				}
		}
	}

	private class AcceptThread extends Thread {
		private BluetoothServerSocket server;

		public AcceptThread() throws IOException {
			server = adapter.listenUsingRfcommWithServiceRecord(NAME, uuid);
		}

		@Override
		public void run() {
			while (!isEstablished())
				try {
					BluetoothSocket client = server.accept();

					try {
						onConnect(this, client);
					} catch (CommunicationAlreadyEstablishedException e1) {
						break;
					} catch (CommunicationNotYetEstablishedException e1) {
						continue;
					} finally {
						client = null;
					}
				} catch (IOException e) {
					Log.e(TAG, "Accept failed", e);

					break;
				}

			close();
		}

		public synchronized void close() {
			if (server != null)
				try {
					server.close();
				} catch (IOException e) {
					Log.e(TAG, "Acceptor server socket closing failed", e);
				} finally {
					server = null;
				}
		}
	}

	private static class Connection {
		private BluetoothSocket socket;

		private ObjectOutputStream output;
		private ObjectInputStream input;

		public Connection(BluetoothSocket socket) throws IOException {
			this.socket = socket;

			output = new ObjectOutputStream(socket.getOutputStream());
			input = new ObjectInputStream(socket.getInputStream());
		}

		public Object read() throws IOException, ClassNotFoundException {
			return input.readObject();
		}

		public void write(Object o) throws IOException {
			output.writeObject(o);
		}

		public synchronized void close() {
			if (output != null)
				try {
					output.close();
				} catch (IOException e) {
					Log.e(TAG, "Connection output stream closing failed", e);
				} finally {
					output = null;
				}

			if (input != null)
				try {
					input.close();
				} catch (IOException e) {
					Log.e(TAG, "Connection input stream closing failed", e);
				} finally {
					input = null;
				}

			if (socket != null)
				try {
					socket.close();
				} catch (IOException e) {
					Log.e(TAG, "Connection socket closing failed", e);
				} finally {
					socket = null;
				}
		}
	}
}